package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.constant.GlobalConstant;
import com.hmdp.entity.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.entity.dto.ExpirationTimeDTO;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.MyUtil;
import com.hmdp.utils.SystemConstants;
import jodd.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    MyUtil myUtil;
    public boolean getLock(String key)
    {
        Boolean flag=redisTemplate.opsForValue().setIfAbsent(key,"1",GlobalConstant.MUTEX_SHOP_ID_TTL, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }
    public void releaseLock(String key)
    {
        redisTemplate.delete(key);
    }
    @Override
    public Result queryShopById(Long id) throws InterruptedException {
        String s = (String)redisTemplate.opsForValue().get(GlobalConstant.SHOP_ID_PREFIX + id.toString());
        if (s!=null)
        {
            JSONObject jsonObject = JSON.parseObject(s);
            Shop shop = jsonObject.toJavaObject(Shop.class);
            return Result.ok(shop);
        }
        LambdaQueryWrapper<Shop> lqw=new LambdaQueryWrapper<>();
        lqw.eq(Shop::getId,id);
        Shop shop = this.getOne(lqw);
        String jsonString = JSON.toJSONString(shop);
        redisTemplate.opsForValue().set(GlobalConstant.SHOP_ID_PREFIX+id.toString(),jsonString);
        return Result.ok(shop);
    }
    @Override
    public Result queryShopByIdWithMutex(Long id) throws InterruptedException {
        String s = (String)redisTemplate.opsForValue().get(GlobalConstant.SHOP_ID_PREFIX + id.toString());
        if (s!=null)
        {
            JSONObject jsonObject = JSON.parseObject(s);
            Shop shop = jsonObject.toJavaObject(Shop.class);
            return Result.ok(shop);
        }
        if(!getLock(GlobalConstant.MUTEX_SHOP_ID+id))
        {
            Thread.sleep(50);
            return this.queryShopByIdWithMutex(id);
        }
        LambdaQueryWrapper<Shop> lqw=new LambdaQueryWrapper<>();
        lqw.eq(Shop::getId,id);
        Shop shop = this.getOne(lqw);
        String jsonString = JSON.toJSONString(shop);
        redisTemplate.opsForValue().set(GlobalConstant.SHOP_ID_PREFIX+id.toString(),jsonString);
        releaseLock(GlobalConstant.MUTEX_SHOP_ID+id);
        return Result.ok(shop);
    }
    @Override
    public Result queryShopByIdWithLogicalExpiration(Long id) throws InterruptedException {
        String s = (String)redisTemplate.opsForValue().get(GlobalConstant.SHOP_ID_PREFIX + id.toString());
        if(s==null)
        {
            return Result.ok();
        }
        JSONObject jsonObject = JSON.parseObject(s);
        ExpirationTimeDTO dto = jsonObject.toJavaObject(ExpirationTimeDTO.class);
        jsonObject=(JSONObject)dto.getObj();
        Shop shop = jsonObject.toJavaObject(Shop.class);
        if(LocalDateTime.now().isAfter(dto.getExpirationTime()))
        {
            if(getLock(GlobalConstant.MUTEX_SHOP_ID+id))
            {
                System.out.println("??");
                 s = (String)redisTemplate.opsForValue().get(GlobalConstant.SHOP_ID_PREFIX + id.toString());
                if(s==null)
                {
                    return Result.ok();
                }
                 jsonObject = JSON.parseObject(s);
                 dto = jsonObject.toJavaObject(ExpirationTimeDTO.class);
                 jsonObject=(JSONObject)dto.getObj();
                 shop=jsonObject.toJavaObject(Shop.class);
                 if(LocalDateTime.now().isAfter(dto.getExpirationTime()))
                 {
                     System.out.println(">>");
                     new Runnable() {
                         @Override
                         public void run() {
                             System.out.println("<<");
                             LambdaQueryWrapper<Shop> lqw=new LambdaQueryWrapper<>();
                             lqw.eq(Shop::getId,id);
                             Shop shop = getOne(lqw);
                             ExpirationTimeDTO dto = new ExpirationTimeDTO();
                             dto.setObj(shop);
                             dto.setExpirationTime(LocalDateTime.now().plusSeconds(10));
                             String jsonString = JSON.toJSONString(dto);
                             try {
                                 Thread.sleep(200);
                             } catch (InterruptedException e) {
                                 throw new RuntimeException(e);
                             }
                             redisTemplate.opsForValue().set(GlobalConstant.SHOP_ID_PREFIX+id.toString(),jsonString);
                             releaseLock(GlobalConstant.MUTEX_SHOP_ID+id);
                         }
                     }.run();
                 }
            }
        }
       return Result.ok(shop);
    }

    @Override
    public Result queryShopByIdWithNullByUtil(Long id) throws InterruptedException {
        return Result.ok(myUtil.getWithNull(GlobalConstant.SHOP_ID_PREFIX+id, Shop.class, new Supplier<Void>() {
            @Override
            public Void get() {
                LambdaQueryWrapper<Shop> lqw=new LambdaQueryWrapper<>();
                lqw.eq(Shop::getId,id);
                Shop shop=getOne(lqw);
                if(shop==null)
                {
                    myUtil.set(GlobalConstant.SHOP_ID_PREFIX+id,"");
                }
                else myUtil.set(GlobalConstant.SHOP_ID_PREFIX+id,shop);
                return null;
            }
        }));
    }

    @Override
    public Result queryShopByIdWithLogicalExpirationByUtil(Long id) throws InterruptedException {
        return Result.ok(myUtil.getWithLogicalExpiration(GlobalConstant.SHOP_ID_PREFIX + id, Shop.class, new Supplier<Void>() {
            @Override
            public Void get() {
                LambdaQueryWrapper<Shop> lqw=new LambdaQueryWrapper<>();
                lqw.eq(Shop::getId,id);
                Shop shop=getOne(lqw);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                myUtil.setWithLogicalExpiration(GlobalConstant.SHOP_ID_PREFIX+id,shop,10L,TimeUnit.SECONDS);
                return null;
            }
        }));
    }

    @Override
    public Result updateShop(Shop shop) {
        this.updateById(shop);
        redisTemplate.delete(GlobalConstant.SHOP_ID_PREFIX+shop.getId().toString());
        return null;
    }

    @Override
    public Result ofType(Long typeId, Integer current, Double x, Double y) {
        if(x==null||y==null)
        {
            Page<Shop> shopPage = this.query().eq("type_id", typeId).page(new Page<>(current, GlobalConstant.DEFAULT_PAGE_SIZE));
            return Result.ok(shopPage.getRecords());
        }
        Integer startIndex=(current-1)*GlobalConstant.DEFAULT_PAGE_SIZE;
        Integer endIndex=current*GlobalConstant.DEFAULT_PAGE_SIZE;
        GeoResults<RedisGeoCommands.GeoLocation<String>> search = redisTemplate.opsForGeo().search(
                GlobalConstant.GEO_SHOP_TYPE + typeId,
                GeoReference.fromCoordinate(x, y),
                new Distance(GlobalConstant.DEFAULT_SEARCH_DISTANCE),
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(endIndex));
        if(search==null)
            return Result.ok(Collections.emptyList());
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = search.getContent();
        if(content==null||content.size()<=startIndex)
            return Result.ok(Collections.emptyList());
        List<Long> ids=new ArrayList<>(content.size());
        Map<String,Distance> map=new HashMap<>();
        content.stream().skip(startIndex).forEach(item->{
            String idStr = item.getContent().getName();
            ids.add(Long.valueOf(idStr));
            Distance distance = item.getDistance();
            map.put(idStr,distance);
        });
        String join = StringUtil.join(ids, ",");
        List<Shop> shops = this.query().in("id", ids).last("order by field(id," + join + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(map.get(shop.getId().toString()).getValue());
        }
        return Result.ok(shops);
//        return null;
    }

}
